import {
    clientSitesAssociationsCache,
    db,
    SiteResource,
    siteResources,
    Transaction
} from "@server/db";
import { clients, orgs, sites } from "@server/db";
import { and, eq, isNotNull } from "drizzle-orm";
import config from "@server/lib/config";
import z from "zod";
import logger from "@server/logger";

interface IPRange {
    start: bigint;
    end: bigint;
}

type IPVersion = 4 | 6;

/**
 * Detects IP version from address string
 */
function detectIpVersion(ip: string): IPVersion {
    return ip.includes(":") ? 6 : 4;
}

/**
 * Converts IPv4 or IPv6 address string to BigInt for numerical operations
 */
function ipToBigInt(ip: string): bigint {
    const version = detectIpVersion(ip);

    if (version === 4) {
        return ip.split(".").reduce((acc, octet) => {
            const num = parseInt(octet);
            if (isNaN(num) || num < 0 || num > 255) {
                throw new Error(`Invalid IPv4 octet: ${octet}`);
            }
            return BigInt.asUintN(64, (acc << BigInt(8)) + BigInt(num));
        }, BigInt(0));
    } else {
        // Handle IPv6
        // Expand :: notation
        let fullAddress = ip;
        if (ip.includes("::")) {
            const parts = ip.split("::");
            if (parts.length > 2)
                throw new Error("Invalid IPv6 address: multiple :: found");
            const missing =
                8 - (parts[0].split(":").length + parts[1].split(":").length);
            const padding = Array(missing).fill("0").join(":");
            fullAddress = `${parts[0]}:${padding}:${parts[1]}`;
        }

        return fullAddress.split(":").reduce((acc, hextet) => {
            const num = parseInt(hextet || "0", 16);
            if (isNaN(num) || num < 0 || num > 65535) {
                throw new Error(`Invalid IPv6 hextet: ${hextet}`);
            }
            return BigInt.asUintN(128, (acc << BigInt(16)) + BigInt(num));
        }, BigInt(0));
    }
}

/**
 * Converts BigInt to IP address string
 */
function bigIntToIp(num: bigint, version: IPVersion): string {
    if (version === 4) {
        const octets: number[] = [];
        for (let i = 0; i < 4; i++) {
            octets.unshift(Number(num & BigInt(255)));
            num = num >> BigInt(8);
        }
        return octets.join(".");
    } else {
        const hextets: string[] = [];
        for (let i = 0; i < 8; i++) {
            hextets.unshift(
                Number(num & BigInt(65535))
                    .toString(16)
                    .padStart(4, "0")
            );
            num = num >> BigInt(16);
        }
        // Compress zero sequences
        let maxZeroStart = -1;
        let maxZeroLength = 0;
        let currentZeroStart = -1;
        let currentZeroLength = 0;

        for (let i = 0; i < hextets.length; i++) {
            if (hextets[i] === "0000") {
                if (currentZeroStart === -1) currentZeroStart = i;
                currentZeroLength++;
                if (currentZeroLength > maxZeroLength) {
                    maxZeroLength = currentZeroLength;
                    maxZeroStart = currentZeroStart;
                }
            } else {
                currentZeroStart = -1;
                currentZeroLength = 0;
            }
        }

        if (maxZeroLength > 1) {
            hextets.splice(maxZeroStart, maxZeroLength, "");
            if (maxZeroStart === 0) hextets.unshift("");
            if (maxZeroStart + maxZeroLength === 8) hextets.push("");
        }

        return hextets
            .map((h) => (h === "0000" ? "0" : h.replace(/^0+/, "")))
            .join(":");
    }
}

/**
 * Converts CIDR to IP range
 */
export function cidrToRange(cidr: string): IPRange {
    const [ip, prefix] = cidr.split("/");
    const version = detectIpVersion(ip);
    const prefixBits = parseInt(prefix);
    const ipBigInt = ipToBigInt(ip);

    // Validate prefix length
    const maxPrefix = version === 4 ? 32 : 128;
    if (prefixBits < 0 || prefixBits > maxPrefix) {
        throw new Error(`Invalid prefix length for IPv${version}: ${prefix}`);
    }

    const shiftBits = BigInt(maxPrefix - prefixBits);
    const mask = BigInt.asUintN(
        version === 4 ? 64 : 128,
        (BigInt(1) << shiftBits) - BigInt(1)
    );
    const start = ipBigInt & ~mask;
    const end = start | mask;

    return { start, end };
}

/**
 * Finds the next available CIDR block given existing allocations
 * @param existingCidrs Array of existing CIDR blocks
 * @param blockSize Desired prefix length for the new block
 * @param startCidr Optional CIDR to start searching from
 * @returns Next available CIDR block or null if none found
 */
export function findNextAvailableCidr(
    existingCidrs: string[],
    blockSize: number,
    startCidr?: string
): string | null {
    if (!startCidr && existingCidrs.length === 0) {
        return null;
    }

    // If no existing CIDRs, use the IP version from startCidr
    const version = startCidr ? detectIpVersion(startCidr.split("/")[0]) : 4; // Default to IPv4 if no startCidr provided

    // Use appropriate default startCidr if none provided
    startCidr = startCidr || (version === 4 ? "0.0.0.0/0" : "::/0");

    // If there are existing CIDRs, ensure all are same version
    if (
        existingCidrs.length > 0 &&
        existingCidrs.some(
            (cidr) => detectIpVersion(cidr.split("/")[0]) !== version
        )
    ) {
        throw new Error("All CIDRs must be of the same IP version");
    }

    // Extract the network part from startCidr to ensure we stay in the right subnet
    const startCidrRange = cidrToRange(startCidr);

    // Convert existing CIDRs to ranges and sort them
    const existingRanges = existingCidrs
        .map((cidr) => cidrToRange(cidr))
        .sort((a, b) => (a.start < b.start ? -1 : 1));

    // Calculate block size
    const maxPrefix = version === 4 ? 32 : 128;
    const blockSizeBigInt = BigInt(1) << BigInt(maxPrefix - blockSize);

    // Start from the beginning of the given CIDR
    let current = startCidrRange.start;
    const maxIp = startCidrRange.end;

    // Iterate through existing ranges
    for (let i = 0; i <= existingRanges.length; i++) {
        const nextRange = existingRanges[i];

        // Align current to block size
        const alignedCurrent =
            current +
            ((blockSizeBigInt - (current % blockSizeBigInt)) % blockSizeBigInt);

        // Check if we've gone beyond the maximum allowed IP
        if (alignedCurrent + blockSizeBigInt - BigInt(1) > maxIp) {
            return null;
        }

        // If we're at the end of existing ranges or found a gap
        if (
            !nextRange ||
            alignedCurrent + blockSizeBigInt - BigInt(1) < nextRange.start
        ) {
            return `${bigIntToIp(alignedCurrent, version)}/${blockSize}`;
        }

        // If next range overlaps with our search space, move past it
        if (nextRange.end >= startCidrRange.start && nextRange.start <= maxIp) {
            // Move current pointer to after the current range
            current = nextRange.end + BigInt(1);
        }
    }

    return null;
}

/**
 * Checks if a given IP address is within a CIDR range
 * @param ip IP address to check
 * @param cidr CIDR range to check against
 * @returns boolean indicating if IP is within the CIDR range
 */
export function isIpInCidr(ip: string, cidr: string): boolean {
    const ipVersion = detectIpVersion(ip);
    const cidrVersion = detectIpVersion(cidr.split("/")[0]);

    // If IP versions don't match, the IP cannot be in the CIDR range
    if (ipVersion !== cidrVersion) {
        // throw new Erorr
        return false;
    }

    const ipBigInt = ipToBigInt(ip);
    const range = cidrToRange(cidr);
    return ipBigInt >= range.start && ipBigInt <= range.end;
}

export async function getNextAvailableClientSubnet(
    orgId: string
): Promise<string> {
    const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));

    if (!org) {
        throw new Error(`Organization with ID ${orgId} not found`);
    }

    if (!org.subnet) {
        throw new Error(`Organization with ID ${orgId} has no subnet defined`);
    }

    const existingAddressesSites = await db
        .select({
            address: sites.address
        })
        .from(sites)
        .where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));

    const existingAddressesClients = await db
        .select({
            address: clients.subnet
        })
        .from(clients)
        .where(and(isNotNull(clients.subnet), eq(clients.orgId, orgId)));

    const addresses = [
        ...existingAddressesSites.map(
            (site) => `${site.address?.split("/")[0]}/32`
        ), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
        ...existingAddressesClients.map(
            (client) => `${client.address.split("/")}/32`
        )
    ].filter((address) => address !== null) as string[];

    const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
    if (!subnet) {
        throw new Error("No available subnets remaining in space");
    }

    return subnet;
}

export async function getNextAvailableAliasAddress(
    orgId: string
): Promise<string> {
    const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));

    if (!org) {
        throw new Error(`Organization with ID ${orgId} not found`);
    }

    if (!org.subnet) {
        throw new Error(`Organization with ID ${orgId} has no subnet defined`);
    }

    if (!org.utilitySubnet) {
        throw new Error(
            `Organization with ID ${orgId} has no utility subnet defined`
        );
    }

    const existingAddresses = await db
        .select({
            aliasAddress: siteResources.aliasAddress
        })
        .from(siteResources)
        .where(
            and(
                isNotNull(siteResources.aliasAddress),
                eq(siteResources.orgId, orgId)
            )
        );

    const addresses = [
        ...existingAddresses.map(
            (site) => `${site.aliasAddress?.split("/")[0]}/32`
        ),
        // reserve a /29 for the dns server and other stuff
        `${org.utilitySubnet.split("/")[0]}/29`
    ].filter((address) => address !== null) as string[];

    let subnet = findNextAvailableCidr(addresses, 32, org.utilitySubnet);
    if (!subnet) {
        throw new Error("No available subnets remaining in space");
    }

    // remove the cidr
    subnet = subnet.split("/")[0];

    return subnet;
}

export async function getNextAvailableOrgSubnet(): Promise<string> {
    const existingAddresses = await db
        .select({
            subnet: orgs.subnet
        })
        .from(orgs)
        .where(isNotNull(orgs.subnet));

    const addresses = existingAddresses.map((org) => org.subnet!);

    const subnet = findNextAvailableCidr(
        addresses,
        config.getRawConfig().orgs.block_size,
        config.getRawConfig().orgs.subnet_group
    );
    if (!subnet) {
        throw new Error("No available subnets remaining in space");
    }

    return subnet;
}

export function generateRemoteSubnets(allSiteResources: SiteResource[]): string[] {
    const remoteSubnets = allSiteResources
        .filter((sr) => {
            if (sr.mode === "cidr") return true;
            if (sr.mode === "host") {
                // check if its a valid IP using zod
                const ipSchema = z.union([z.ipv4(), z.ipv6()]);
                const parseResult = ipSchema.safeParse(sr.destination);
                return parseResult.success;
            }
            return false;
        })
        .map((sr) => {
            if (sr.mode === "cidr") return sr.destination;
            if (sr.mode === "host") {
                return `${sr.destination}/32`;
            }
            return ""; // This should never be reached due to filtering, but satisfies TypeScript
        })
        .filter((subnet) => subnet !== ""); // Remove empty strings just to be safe
    // remove duplicates
    return Array.from(new Set(remoteSubnets));
}

export type Alias = { alias: string | null; aliasAddress: string | null };

export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
    let aliasConfigs = allSiteResources
        .filter((sr) => sr.alias && sr.aliasAddress && sr.mode == "host")
        .map((sr) => ({
            alias: sr.alias,
            aliasAddress: sr.aliasAddress
        }));
    return aliasConfigs;
}

export type SubnetProxyTarget = {
    sourcePrefix: string; // must be a cidr
    destPrefix: string; // must be a cidr
    rewriteTo?: string; // must be a cidr
    portRange?: {
        min: number;
        max: number;
    }[];
};

export function generateSubnetProxyTargets(
    siteResource: SiteResource,
    clients: {
        clientId: number;
        pubKey: string | null;
        subnet: string | null;
    }[]
): SubnetProxyTarget[] {
    const targets: SubnetProxyTarget[] = [];

    if (clients.length === 0) {
        logger.debug(
            `No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
        );
        return [];
    }

    for (const clientSite of clients) {
        if (!clientSite.subnet) {
            logger.debug(
                `Client ${clientSite.clientId} has no subnet, skipping for site resource ${siteResource.siteResourceId}.`
            );
            continue;
        }

        const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;

        if (siteResource.mode == "host") {
            let destination = siteResource.destination;
            // check if this is a valid ip
            const ipSchema = z.union([z.ipv4(), z.ipv6()]);
            if (ipSchema.safeParse(destination).success) {
                destination = `${destination}/32`;

                targets.push({
                    sourcePrefix: clientPrefix,
                    destPrefix: destination
                });
            }

            if (siteResource.alias && siteResource.aliasAddress) {
                // also push a match for the alias address
                targets.push({
                    sourcePrefix: clientPrefix,
                    destPrefix: `${siteResource.aliasAddress}/32`,
                    rewriteTo: destination
                });
            }
        } else if (siteResource.mode == "cidr") {
            targets.push({
                sourcePrefix: clientPrefix,
                destPrefix: siteResource.destination
            });
        }
    }

    // print a nice representation of the targets
    // logger.debug(
    //     `Generated subnet proxy targets for: ${JSON.stringify(targets, null, 2)}`
    // );

    return targets;
}
